<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta
      http-equiv="origin-trial"
      content="Aq6vv/4syIkcyMszFgCc9LlH0kX88jdE7SXfCFnh2RQN0nhhL8o6PCQ2oE3a7n3mC7+d9n89Repw5HYBtjarDw4AAAB3eyJvcmlnaW4iOiJodHRwczovL3B5b2RpZGUub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseUpTUHJvbWlzZUludGVncmF0aW9uIiwiZXhwaXJ5IjoxNzMwMjQ2Mzk5LCJpc1N1YmRvbWFpbiI6dHJ1ZX0="
    />
    <meta
      http-equiv="origin-trial"
      content="Ai8IXb0XqedlM/Q2guWXFfBkKiYY9uaPZpdjHqc8y0ZvpAfK9SKzp/dIuFH+txG/HEKxt59uIkk39hhWrhNgbw4AAABieyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJmZWF0dXJlIjoiV2ViQXNzZW1ibHlKU1Byb21pc2VJbnRlZ3JhdGlvbiIsImV4cGlyeSI6MTczMDI0NjM5OX0="
    />
    <script src="https://cdn.jsdelivr.net/npm/jquery"></script>
    <script src="https://cdn.jsdelivr.net/npm/jquery.terminal@2.35.2/js/jquery.terminal.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/jquery.terminal@2.35.2/js/unix_formatting.min.js"></script>
    <link
      href="https://cdn.jsdelivr.net/npm/jquery.terminal@2.35.2/css/jquery.terminal.min.css"
      rel="stylesheet"
    />
    <style>
      .terminal {
        --size: 1.5;
        --color: rgba(255, 255, 255, 0.8);
      }
      .noblink {
        --animation: terminal-none;
      }
      body {
        background-color: black;
      }
      #jquery-terminal-logo {
        color: white;
        border-color: white;
        position: absolute;
        top: 7px;
        right: 18px;
        z-index: 2;
      }
      #jquery-terminal-logo a {
        color: gray;
        text-decoration: none;
        font-size: 0.7em;
      }
      #loading {
        display: inline-block;
        width: 50px;
        height: 50px;
        position: fixed;
        top: 50%;
        left: 50%;
        border: 3px solid rgba(172, 237, 255, 0.5);
        border-radius: 50%;
        border-top-color: #fff;
        animation: spin 1s ease-in-out infinite;
        -webkit-animation: spin 1s ease-in-out infinite;
      }

      @keyframes spin {
        to {
          -webkit-transform: rotate(360deg);
        }
      }
      @-webkit-keyframes spin {
        to {
          -webkit-transform: rotate(360deg);
        }
      }
    </style>
  </head>
  <body>
    <div id="jquery-terminal-logo">
      <a href="https://terminal.jcubic.pl/">jQuery Terminal</a>
    </div>
    <div id="loading"></div>
    <script>
      "use strict";

      function sleep(s) {
        return new Promise((resolve) => setTimeout(resolve, s));
      }

      async function main() {
        let indexURL = "{{ PYODIDE_BASE_URL }}";
        const urlParams = new URLSearchParams(window.location.search);
        const buildParam = urlParams.get("build");
        if (buildParam) {
          if (["full", "debug", "pyc"].includes(buildParam)) {
            indexURL = indexURL.replace(
              "/full/",
              "/" + urlParams.get("build") + "/",
            );
          } else {
            console.warn(
              'Invalid URL parameter: build="' +
                buildParam +
                '". Using default "full".',
            );
          }
        }
        const { loadPyodide } = await import(indexURL + "pyodide.mjs");
        // to facilitate debugging
        globalThis.loadPyodide = loadPyodide;

        let term;
        globalThis.pyodide = await loadPyodide({
          stdin: () => {
            let result = prompt();
            echo(result);
            return result;
          },
        });
        let { repr_shorten, BANNER, PyodideConsole } =
          pyodide.pyimport("pyodide.console");
        BANNER =
          `Welcome to the Pyodide ${pyodide.version} terminal emulator 🐍\n` +
          BANNER;
        const pyconsole = PyodideConsole(pyodide.globals);

        const namespace = pyodide.globals.get("dict")();
        const await_fut = pyodide.runPython(
          `
          import builtins
          from pyodide.ffi import to_js

          async def await_fut(fut):
              res = await fut
              if res is not None:
                  builtins._ = res
              return to_js([res], depth=1)

          await_fut
          `,
          { globals: namespace },
        );
        namespace.destroy();

        const echo = (msg, ...opts) =>
          term.echo(
            msg
              .replaceAll("]]", "&rsqb;&rsqb;")
              .replaceAll("[[", "&lsqb;&lsqb;"),
            ...opts,
          );

        const ps1 = ">>> ";
        const ps2 = "... ";

        async function lock() {
          let resolve;
          const ready = term.ready;
          term.ready = new Promise((res) => (resolve = res));
          await ready;
          return resolve;
        }

        async function interpreter(command) {
          const unlock = await lock();
          term.pause();
          // multiline should be split (useful when pasting)
          for (const c of command.split("\n")) {
            const escaped = c.replaceAll(/\u00a0/g, " ");
            const fut = pyconsole.push(escaped);
            term.set_prompt(fut.syntax_check === "incomplete" ? ps2 : ps1);
            switch (fut.syntax_check) {
              case "syntax-error":
                term.error(fut.formatted_error.trimEnd());
                continue;
              case "incomplete":
                continue;
              case "complete":
                break;
              default:
                throw new Error(`Unexpected type ${ty}`);
            }
            // In JavaScript, await automatically also awaits any results of
            // awaits, so if an async function returns a future, it will await
            // the inner future too. This is not what we want so we
            // temporarily put it into a list to protect it.
            const wrapped = await_fut(fut);
            // complete case, get result / error and print it.
            try {
              const [value] = await wrapped;
              if (value !== undefined) {
                echo(
                  repr_shorten.callKwargs(value, {
                    separator: "\n<long output truncated>\n",
                  }),
                );
              }
              if (value instanceof pyodide.ffi.PyProxy) {
                value.destroy();
              }
            } catch (e) {
              if (e.constructor.name === "PythonError") {
                const message = fut.formatted_error || e.message;
                term.error(message.trimEnd());
              } else {
                throw e;
              }
            } finally {
              fut.destroy();
              wrapped.destroy();
            }
          }
          term.resume();
          await sleep(10);
          unlock();
        }

        term = $("body").terminal(interpreter, {
          greetings: BANNER,
          prompt: ps1,
          completionEscape: false,
          completion: function (command, callback) {
            callback(pyconsole.complete(command).toJs()[0]);
          },
          keymap: {
            "CTRL+C": async function (event, original) {
              pyconsole.buffer.clear();
              term.enter();
              echo("KeyboardInterrupt");
              term.set_command("");
              term.set_prompt(ps1);
            },
            TAB: (event, original) => {
              const command = term.before_cursor();
              // Disable completion for whitespaces.
              if (command.trim() === "") {
                term.insert("\t");
                return false;
              }
              return original(event);
            },
          },
        });
        window.term = term;
        pyconsole.stdout_callback = (s) => echo(s, { newline: false });
        pyconsole.stderr_callback = (s) => {
          term.error(s.trimEnd());
        };
        term.ready = Promise.resolve();
        pyodide._api.on_fatal = async (e) => {
          if (e.name === "Exit") {
            term.error(e);
            term.error("Pyodide exited and can no longer be used.");
          } else {
            term.error(
              "Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.",
            );
            term.error("The cause of the fatal error was:");
            term.error(e);
            term.error("Look in the browser console for more details.");
          }
          await term.ready;
          term.pause();
          await sleep(15);
          term.pause();
        };

        const searchParams = new URLSearchParams(window.location.search);
        if (searchParams.has("noblink")) {
          $(".cmd-cursor").addClass("noblink");
        }

        let idbkvPromise;
        async function getIDBKV() {
          if (!idbkvPromise) {
            idbkvPromise = await import(
              "https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js"
            );
          }
          return idbkvPromise;
        }

        async function mountDirectory(pyodideDirectory, directoryKey) {
          if (pyodide.FS.analyzePath(pyodideDirectory).exists) {
            return;
          }
          const { get, set } = await getIDBKV();
          const opts = {
            id: "mountdirid",
            mode: "readwrite",
          };
          let directoryHandle = await get(directoryKey);
          if (!directoryHandle) {
            directoryHandle = await showDirectoryPicker(opts);
            await set(directoryKey, directoryHandle);
          }
          const permissionStatus =
            await directoryHandle.requestPermission(opts);
          if (permissionStatus !== "granted") {
            throw new Error("readwrite access to directory not granted");
          }
          await pyodide.mountNativeFS(pyodideDirectory, directoryHandle);
        }
        globalThis.mountDirectory = mountDirectory;
      }
      window.console_ready = main();
    </script>
  </body>
</html>
